\documentclass[a4paper,11pt]{article}

\usepackage{lscape}
\usepackage{geometry}
\usepackage[utf8]{inputenc}
\usepackage[francais]{babel}
\usepackage[T1]{fontenc}
\usepackage{tikz}
\usepackage{relsize}
\usepackage{color}
\usepackage[boxed]{algorithm2e}
\usepackage[hidelinks]{hyperref}
\definecolor{dkgreen}{rgb}{0,0.6,0}
\definecolor{gray}{rgb}{0.5,0.5,0.5}
\definecolor{mauve}{rgb}{0.58,0,0.82}

\usepackage{listings}
\usepackage{float}
%\usepackage{kpfonts}

\usepackage{graphicx}
%\usepackage{rotating}

\lstset{
  language=C++,
  basicstyle=\footnotesize,
  backgroundcolor=\color{white},
  keywordstyle=\color{red},
  commentstyle=\color{dkgreen},
  stringstyle=\color{mauve},
  numberstyle=\color{red},
  morekeywords={string},
  frame=BL,
  aboveskip=1em,
  belowskip=2em,
}
\lstset{
  literate={ù}{{\`u}}1
  {é}{{\'e}}1
  {è}{{\'e}}1
  {à}{{\`a}}1
}


\lstdefinelanguage{tikzuml}{language=[LaTeX]TeX, classoffset=0, morekeywords={umlbasiccomponent, umlprovidedinterface, umlrequiredinterface, umldelegateconnector, umlassemblyconnector, umlVHVassemblyconnector, umlHVHassemblyconnector, umlnote, umlusecase, umlactor, umlinherit, umlassoc, umlVHextend, umlinclude, umlstateinitial, umlbasicstate, umltrans, umlstatefinal, umlVHtrans, umlHVtrans, umldatabase, umlmulti, umlobject, umlfpart, umlcreatecall, umlclass, umlvirt, umlunicompo, umlimport, umlaggreg}, classoffset=1, morekeywords={umlcomponent, umlsystem, umlstate, umlseqdiag, umlcall, umlcallself, umlfragment, umlpackage}, classoffset=0,  sensitive=true, morecomment=[l]{\%}}

\geometry{margin=2.5cm}
\geometry{headheight=15pt}

\usepackage{fancyhdr}
\usepackage{fancyvrb}
\usepackage{float}
\usepackage[footnote,smaller]{acronym}

\pagestyle{fancy}
\rhead{IT202 - Projet de Système d'Exploitation}

% \acrodef{LABRI}{Laboratoire Bordelais de Recherche en Informatique}

\begin{document}

\begin{titlepage}
  \begin{center}

    \textsc{IT202 - Projet de Système d'Exploitation}\\[2cm]
    \textsc{\large Rapport Final}\\[3cm]
    Maxime \textsc{Bellier} \ \ \ Louis \textsc{Boucherie}\ \ \ Jean-Michaël \textsc{Celerier}\\
    Julien \textsc{Chaumont} \ \ \ Bazire \textsc{Houssin} \ \ \ Sylvain \textsc{Vaglica}\\[1cm]
    \textsc{Groupe 3}\\[1.5cm]
    \textsc{\large 16/05/2013 }\\[1.5cm]
    \includegraphics[width=8cm]{logo.png}

  \end{center}
  \vspace{3cm}

\end{titlepage}

\clearpage

\section*{Introduction}

Le projet de Système d'Exploitation consiste en la réalisation d'une bibliothèque de threads. Les tests ayant déjà été implémentés et fixés en amont, il s'agit d'un développement dirigé par les tests.


%objectifs

\section{Définition des structures de données}

L'une des premières étapes aura été de définir les structures de données à adopter à partir des résultats attendus et de l'implémentation des tests. Cette première partie décrira de manière détaillée les structures choisies.

\subsection{Le type \texttt{thread\_t}}

Le type \texttt{thread\_t} est en fait à la base de notre implémentation: cette structure représente un thread en mémoire. Sa définition est donnée en figure \ref{threadt}.

\begin{figure}[H]
\begin{lstlisting}
typedef enum {READY, SLEEPING, DEAD} STATE;

typedef struct thread_t_
{
  STATE state;
  ucontext_t context;
  void *retval;
  int valgrind_stackid;
  TAILQ_ENTRY(thread_t_) entries;
} *thread_t;
\end{lstlisting}
\caption{Implémentation de \texttt{thread\_t}}
\label{threadt}
\end{figure}

Ses attributs ont chacun leur utilité:
\begin{itemize}
  \item \texttt{state}: état du thread, à savoir \emph{prêt à l'exécution}, \emph{endormi}, ou \emph{mort};
  \item \texttt{context}: contexte d'exécution du thread, géré à l'aide des fonctions de la bibliothèque standard \texttt{getcontext}, \texttt{setcontext}, \texttt{makecontext} et \texttt{swapcontext}, déclarées dans \texttt{ucontext.h};
  \item \texttt{retval}: pointeur vers la valeur de retour de la fonction exécutée dans le thread;
  \item \texttt{valgrind\_stackid}: permet à l'utilitaire \textit{Valgrind} de mieux gérer la détection de problèmes de mémoire dans les piles;
  \item \texttt{entries}: structure contenant des pointeurs vers les threads suivant et précédent. Il s'agit en fait d'une liste doublement chaînée utilisant l'implémentation de \textit{Queue BSD}\footnote{http://linux.die.net/include/sys/queue.h}.
\end{itemize}

\subsection{Le type \texttt{Threads}}

Le type \texttt{Threads} a été mis en place afin de gérer différents threads au cours de l'exécution d'un programme. Il s'agit en fait d'une file contenant les différents threads créés par le programmeur, ce qui permet de passer aisément d'un thread à un autre.

Comme évoqué précédemment, c'est l'implémentation de \textit{Queue BSD} qui est utilisée ici. Cette structure de file assure le passage d'un thread vers le thread suivant ou précédent en temps constant (situation d'un changement de contexte \textit{``naturel''}). La définition du type \texttt{Threads} est donnée en figure \ref{threadst}.

\begin{figure}[H]
\begin{lstlisting}
typedef struct Threads
{
  int is_initialized;
  thread_t main_thread;
  thread_t current_thread;
  TAILQ_HEAD(, thread_t_) list;
  TAILQ_HEAD(, thread_t_) list_sleeping;
  TAILQ_HEAD(, thread_t_) list_dead;
} Threads;
\end{lstlisting}
\caption{Implémentation du type \texttt{Threads}}
\label{threadst}
\end{figure}

Le rôle de chaque attribut est le suivant:
\begin{itemize}
  \item \texttt{is\_initialized}: booléen indiquant si la liste a déjà été initialisée par la création d'un premier thread ou non;
  \item \texttt{main\_thread}: thread correspondant au contexte principal du programme (celui ayant été créé lors de l'initialisation);
  \item \texttt{current\_thread}: thread en train d'être exécuté;
  \item \texttt{list}: liste des threads prêts à être exécutés;
  \item \texttt{list\_sleeping}: liste des threads endormis;
  \item \texttt{list\_dead}: liste des threads morts.
\end{itemize}

\subsection{Fonctionnement}

L'initialisation des structures de données a lieu lors du premier appel à l'une des fonctions de la bibliothèque. Pour plus de facilité, la structure \texttt{Threads} utilisée est en fait définie de manière statique avec une portée globale.

\subsubsection*{L'initialisation}

L'initialisation de \texttt{Threads} consiste simplement en la création des différentes listes en attribut, ainsi qu'en l'initialisation d'un premier thread correspondant au contexte d'appel. Ce thread sera défini comme étant le thread principal.

De plus, l'instruction suivante:
\begin{center}
\verb[atexit(threads_destroy);[
\end{center}
assure la libération mémoire de l'intégralité des allocations dynamiques effectuées à la fin du programme.

\subsubsection*{Création d'un thread}

La création d'un thread se résume en l'allocation dynamique d'une variable \texttt{thread\_t} puis en son initialisation. Il ne reste ensuite qu'à l'insérer à la fin de la liste des threads prêts de la structure \texttt{Threads}.

\subsubsection*{Passage de main}

Le passage de main, à l'aide de la fonction \texttt{thread\_yield}, consiste simplement à placer le thread courant en fin de liste et d'exécuter le thread se trouvant en début de liste. Dans le cas où cette liste serait vide, il faut réveiller un thread endormi pour l'exécuter. Cette fonction se charge d'effectuer le changement de contexte, ce qui lance l'exécution de la fonction associée au thread.

\subsubsection*{Attente de thread}

L'attente d'un thread consiste en fait à attendre qu'il passe à l'état \textit{mort}. Si jamais le thread concerné n'est pas le thread courant, le remplacement est effectué, suivi par le changement de contexte, afin de reprendre son exécution. S'il est endormi, il faut alors le réveiller, le placer dans la liste des threads prêts, et l'exécuter. La valeur de retour du thread est récupérée directement depuis l'attribut \texttt{retval} de la structure \texttt{thread\_t}.

\subsubsection*{Terminer un thread}

Lorsque l'utilisateur demande à terminer un thread, il est simplement indiqué comme mort et placé dans la liste des threads morts. Il ne reste alors qu'à demander à passer la main.





\section{Choix des objectifs avancés}

L’ensemble des fonctions de base figurant dans le fichier \texttt{thread.h} fourni a été implémenté. Les tests pré-existants ont permis de vérifier au fur et à mesure du développement que le code en cours
d’écriture était en adéquation avec les prérequis.\\

Les objectifs avancés envisagés ensuite sont présentés dans les sections suivantes. Plusieurs d'entre eux ont été développés parallèlement, grâce au système de branches proposé par le gestionnaire de versions \textit{Git} : d'une part les priorités et la préemption, et d'autre part le support des machines multiprocesseurs.

\section{Préemption}

\subsection{Principe et utilité}

Afin de mieux intégrer le système de priorités élaboré précédemment, il fallait mettre en place une gestion de la préemption. En effet, sans préemption, les priorités n'opèrent que lorsque le thread courant \textit{choisit} de passer la main. On peut par exemple imaginer un thread non prioritaire s'exécutant sans jamais appeler au \textit{yield}, qui s'exécuterait jusqu'à sa terminaison.

La préemption pallie ce problème en offrant, en quelque sorte, un \textit{arbitre} qui force le passage de main. Ainsi, la préemption rend le système de priorités utile dans tous les cas, et non plus seulement dans le cadre d'un code coopératif (\textit{ie} dont les fonctions font des \textit{yields} de leur propre chef et de manière régulière).

\subsection{Les bases de l'implémentation}

L'idée de base consiste simplement à voir l'ordonnanceur comme un superviseur s'exécutant en parallèle des threads. Pour permettre cette exécution simultanée, la mise en place s'est faite à l'aide d'un thread noyau (bibliothèque \texttt{pthread}) propre à l'ordonnanceur, tandis que les threads utilisateurs étaient exécutés depuis le thread noyau principal.

\subsubsection*{La remise à plat du système de priorités}

Comme évoqué dans le rapport précédent, le système de priorités mis en place s'avérait être complexe dans certains cas, notamment lorsque la différence de priorité entre deux threads était trop importante. L'algorithme donné en figure \ref{priority1} correspond à cette première version.

\begin{figure}[H]
\begin{algorithm}[H]
 thread = thread\_next(thread\_list$\rightarrow$current\_thread)\;
 continue = true\;
 \While{continue} {
 	thread$\rightarrow$current\_priority = thread$\rightarrow$current\_priority - 1\;
 	\tcc{Reset priority}
 	\If{thread$\rightarrow$current\_priority == thread$\rightarrow$default\_priority - thread\_list$\rightarrow$max\_priority} {
 			thread$\rightarrow$current\_priority = thread$\rightarrow$default\_priority\;
 		}
    \tcc{This thread can be executed}
 	\eIf{thread$\rightarrow$current\_priority > 0} {
 		continue = false\;
 	}{
 	\tcc{Otherwise, another try with the next one}
 	thread = thread\_next(thread)\;
 	}
  }
  execute(thread)\;
\end{algorithm}
\caption{Ordonnancement avec la première version des priorités}
\label{priority1}
\end{figure}

Néanmoins, en restreignant l'intervalle possible des priorités sur un petit intervalle, cet algorithme s'exécute avec une complexité plutôt satisfaisante:
\begin{itemize}
 \item En temps constant si tous les threads ont la même priorité
 \item En temps constant si tous les threads ont une priorité courante positive
 \item En temps linéaire dans le pire des cas (parcours de la liste entière pour finalement retomber sur le thread qu'on venait de quitter)
\end{itemize}

La seule contrainte est de devoir garder une valeur correcte pour la priorité maximale dans la liste des threads, ce qui impose un parcours complet de la liste dès lors qu'un thread dont la priorité est égale à la priorité maximale sort de la liste (qu'il soit mort ou endormi). L'ajout d'un thread dont la priorité est plus importante que les autres n'implique pas de nouveau parcours.\\

Le nouveau système mis en place améliore encore cette complexité, puisqu'elle assure un \textit{yield} en temps constant. Plutôt que de se baser sur le nombre de \textit{yields} sur un thread donné, il suffit de travailler sur la durée d'exécution de ce même thread entre deux \textit{yields}: plus un thread est prioritaire, plus il aura droit à une durée élevée pour s'exécuter avant le prochain ordonnancement. L'algorithme est donné en figure \ref{priority2}.

\begin{figure}[H]
\begin{algorithm}[H]
 \While{thread\_list $\neq$ $\emptyset$}{
 	thread = thread\_next(thread\_list$\rightarrow$current\_thread)\;
 	ms = execution\_time(thread$\rightarrow$priority)\;
 	execute(thread);\tcp{Executed in another kernel thread}
 	sleep(ms);\tcp{Main thread}
 	save\_context\_and\_pause(thread)\;
 }
\end{algorithm}
\caption{Ordonnancement avec la seconde version - point de vue algorithmique}
\label{priority2}
\end{figure}

La raison pour laquelle cette version n'a pas été celle qui a été initialement choisie est que la première version mise en place ne nécessitait pas de préemption, contrairement à la seconde. En effet, dans cette nouvelle situation, l'ordonnanceur doit mesurer la durée d'exécution de chaque thread pour demander le \textit{yield} au moment adéquat.

\subsubsection*{Implémentation}

Si le fonctionnement général de l'ordonnancement décrit ci-avant est relativement simple à comprendre, il n'en est pas de même pour son implémentation. Sur les différents essais effectués, le point commun est l'utilisation d'un thread noyau pour faire tourner une fonction faisant les appels \textit{yields}. La première méthode mise en place consistait en l'utilisation de signaux (voir figure \ref{codepreemption1}).

\begin{figure}[H]
\begin{lstlisting}
void call_yield() {
  thread_yield();
}

void* preemption_signal(void* n) {
  //Association between the SIG_YIELD signal and the call_yield function
  signal(SIG_YIELD, call_yield);

  while(run_preemption) {
    //Send a SIG_YIELD signal and wait
    raise(SIG_YIELD);
    sleep(1);
  }
  return n;
}

void start_preemption() {
  run_preemption = 1;
  pthread_create(&preemption_thread, NULL, (void*) preemption_signal, (void*) 0);
}

void stop_preemption() {
  run_preemtion = 0;
  pthread_join(&preemption_thread);
}
\end{lstlisting}
\caption{Premier essai d'implémentation}
\label{codepreemption1}
\end{figure}

Ce code ne fonctionne pas, pour la simple raison que lorsque le signal \texttt{SIG\_YIELD} est émis, c'est dans \texttt{preemption\_thread} qu'il s'exécute. Une deuxième version utilisant les fonctions de la bibliothèque \texttt{pthread} a donc été testée (figure \ref{codepreemption2}.

\begin{figure}[H]
\begin{lstlisting}
void* preemption_signal(void* n) {
  while(run_preemption) {
    //Starting a user thread in another pthread and wait
    pthread_create(&waiting_thread, NULL, (void*) call_yield, (void*) 0);
    sleep(1);
    //Stop the pthread
    pthread_cancel(waiting_thread);
  }
  return n;
}
\end{lstlisting}
\caption{Deuxième essai}
\label{codepreemption2}
\end{figure}

Là encore, un problème se pose. En effet, bien que cette solution permette effectivement d'exécuter à la fois la fonction \texttt{preemption\_signal} dans un thread noyau, et les threads utilisateurs dans un autre, l'arrêt de ce dernier est pour le moins\dots\ brutal. En effet, la fonction \texttt{pthread\_cancel} ne permet pas de récupérer le contexte du thread, ce qui interdit toute tentative de le reprendre ultérieurement. Il s'agit en fait plus d'une manière de quitter que de mettre en pause.

Malheureusement, par manque de temps, et sûrement aussi d'inspiration, la branche du dépôt consacrée à la préemption en est restée là, à quelques nouvelles tentatives plus ou moins hasardeuses prêt.

\subsubsection*{Sans préemption, améliorer la complexité des priorités}

Une fois la mise en place de la préemption abandonnée, une partie du temps restant a été consacrée à tenter d'améliorer la complexité du système de priorités. L'idée retenue était de créer non plus une unique liste de threads, mais une par priorité. L'algorithme correspondant est donné en figure \ref{priority3}.

\begin{figure}[H]
\begin{algorithm}[H]
 thread = thread\_list$\rightarrow$current\_thread\;
 priority = thread$\rightarrow$priority\;
 continue = true\;
 static count = 0;\tcp{Must keep its value from one call to another}
 \BlankLine
 \While{thread\_list $\neq$ $\emptyset$ \textbf{and} continue}{
	first\_thread = thread\_list$\rightarrow$list[priority][0]\;
 	thread = thread\_next(thread, thread\_list$\rightarrow$list[priority])\;
 	\tcc{If we executed the whole list}
 	\If{thread == first\_thread}{
 		count = count + 1\;
 	}
 	\tcc{If we executed the whole list "priority" times}
 	\eIf{count == priority} {
 	    \tcc{We consider the next priority}
 		count = 0\;
 		\eIf{priority == 0} {
 			priority = thread\_list$\rightarrow$max\_priority\;
 		}{
 			priority = priority - 1\;
 		}
 	}{
 	    \tcc{Otherwise, we've found the thread to execute next!}
 		continue = false\;
 	}
 }
 execute(thread)\;
\end{algorithm}
\caption{Améliorer le système de priorités}
\label{priority3}
\end{figure}

Brièvement, cette solution consiste simplement à exécuter tous les threads de chaque liste autant de fois que la valeur de leur priorité, et quand toute la liste a été exécutée ainsi, on peut passer à la liste de priorité suivante.

L'implémentation de cette méthode n'est pas difficile en soit, mais nécessite tout de même un nombre de modifications non négligeable dans le code. Le temps perdu à tenter de mettre en place la préemption a empêché la mise en place de ce nouveau système (les tentatives de dernière minute étaient trop buggées pour être corrigées d'ici la date de rendu du projet), et c'est pourquoi, a posteriori, nous regrettons de ne pas avoir commencé par l'implémenter avant de passer à la préemption.

\section{Multithread}

Cette section expose l'implémentation faite des threads noyaux pour exécuter simultanément les threads utilisateurs, lorsqu'une machine dispose
de plusieurs unités d'exécution parallèles.

\subsection{Réflexion et implémentation}

De manière simplifiée, au lieu d'un seul thread noyau, un tableau de threads noyaux sera utilisé, et chaque fois qu'un \texttt{thread\_yield} sera fait sur l'un des threads noyaux, le thread en tête de liste des threads prêts sera chargé. Derrière ce principe simple se cachent néanmoins des problèmes à résoudre.

\subsubsection*{Changements préalables}

Un changement a dû être opéré avant même de commencer la gestion multithread. Dans une première implémentation, le thread courant était stocké dans le champ \texttt{currentThread} de la structure \texttt{Threads} mais était aussi laissé en tête de la liste des threads prêts. Ceci devient impossible lorsqu'il y a plusieurs threads courants simultanément. Ainsi, lorsqu'un thread est chargé par un thread noyau, il est maintenant retiré de la liste des threads prêts.

Le second point à changer avant de pouvoir réellement commencer les multithreads est la protection des données. Pour éviter par exemple qu'un thread utilisateur soit attribué à deux threads noyaux en même temps, les accès à la liste des threads doivent être gérés. Ainsi, les sections critiques ont été identifiées et encadrées par des verrouillages et déverrouillages de \textit{spinlocks}. Ce sont toutes les zones du codes dans lesquelles sont faits des tests sur les listes de threads ou des modifications sur ces listes.

\subsubsection*{Implémentation du multithreading}

Concernant la gestion du multithreading, dans la structure \texttt{Threads}, le champ \texttt{currentThread} n'est plus de type \texttt{thread\_t} mais de type \texttt{thread\_t *}. Le champ \texttt{pthread\_t *pthreadTList} y a été ajouté : ce tableau stockera les threads noyaux. Les deux tableaux précédents étant de même taille, le thread stocké dans la case $i$ de \texttt{currentThread} est le thread en cours d'exécution dans le thread noyau stocké dans la case $i$ de \texttt{pthreadTList}. On remarque que l'on utilise la bibliothèque \texttt{pthread} pour les threads noyaux et les verrous.

Par choix, le tableau des threads noyaux est de taille le nombre de c\oe urs de la machine. Ce nombre est lu dans le fichier \texttt{/proc/cpuinfo} par la fonction \texttt{get\_cores}.

Les autres modifications importantes du code existant ont été faites à l'initialisation des tableaux de threads courants et pthreads, et dans la fonction \texttt{thread\_yield}.

Le tableau des threads courants est initialisé à \texttt{NULL}, sauf la dernière case qui contient le thread du main. Les threads noyaux sont initialisés avec la fonction \texttt{initPthread} qui lui attribue un numéro unique et fait un appel à \texttt{thread\_yield}.

La fonction \texttt{thread\_yield} a donc dû être modifiée pour gérer le cas dans lequel le thread courant lors de l'appel vaut \texttt{NULL} (\textit{ie} est non initialisé). Ainsi, après modification et dans ce cas là, la fonction \texttt{thread\_yield} effectue un \texttt{setcontext} au lieu d'un \texttt{swapcontext}.

Enfin, le système de clefs de pthread (avec les fonctions pthread\_key\_create) a été utilisé pour identifier les threads noyaux,
dans le tableau de la structure \texttt{Threads}.

\subsubsection*{Remarque}

L'entier unique attribué à chaque thread noyaux est généré par la fonction \texttt{pthread\_setspecific}.

\subsection{Problèmes du multithreading}

Avant tout, il convient de reconnaître que les essais effectués pour implémenter un multithreading
fonctionnel se sont soldés par un échec.

Certainement en raison de problèmes de concurrence, le code échoue en effet de manière aléatoire à différents
endroits du programme.
Il arrive même parfois que tous les tests passent.
Mais le cas le plus fréquent est l'erreur de segmentation, ou le core dump.

\subsubsection{Problèmes lors de la terminaison}
Des difficultés ont été rencontrées pour terminer proprement le programme.
En effet, l'appel de la fonction \texttt{atexit()} pouvant s'exécuter sur n'importe quel thread,
il est arrivé que des threads essayent de se libérer eux-mêmes, ce qui se solde par l'échec de l'exécution.

\subsubsection{Problèmes de deadlock}
Des deadlocks ont été rencontrées à plusieurs étapes.
Ont été protégés à l'aide des mutex pthread tous les accès aux listes de threads, vivants, endormis, et morts.
Cela pour éviter par exemple qu'un thread vivant soit envoyé par erreur dans la liste des threads morts.

% Ca marche pas.
% L'erreur obtenue change d'une exécution à l'autre.
% Ca fait des segfault.
% L'erreur semble provenir de ...

\input{partie3.tex}

\section*{Conclusion}

Bien que le projet ait rapidement atteint les objectifs principaux, trop de temps a été perdu sur des tentatives infructueuses d'implémentation d'objectifs secondaires, ce qui explique finalement le peu de différences entre le code final et le code au moment du rapport intermédiaire. Tout n'est pas négatif pour autant, puisque ce projet aura permis de mieux appréhender certains concepts \textit{"bas niveau"} que nous n'avions pas abordés jusqu'alors. Les points non implémentés dans la version finale, à défaut d'avoir été insérés avec succès, ont tout de même été mûrement réfléchis et testés avant d'être abandonnés.

Par ailleurs, le projet rendu répond à toutes les demandes principales du sujet, en plus d'un système de priorités fonctionnel mis en place. Ceci se traduit notamment par le passage de tous les tests imposés, avec des performances satisfaisantes (en comparaison de la bibliothèque de threads noyaux \texttt{pthread}). On notera également l'approbation de l'utilitaire \textit{Valgrind} quant aux fuites mémoires engendrées par la bibliothèque. Au niveau du code, un soin particulier a été apporté à sa lisibilité et à son organisation.

Finalement, bien que nous ne soyons par parvenus à atteindre les objectifs que nous nous étions initialement fixés, et malgré quelques déceptions à ce propos, nous sommes parvenus à mettre en place une bibliothèque de threads utilisateurs fonctionnelle, et globalement satisfaisante.


\end{document}
